/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package cloudclient.controller;

import cloudclient.helper.Utilities;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.table.DefaultTableModel;
import org.progin.cloudfolder.msg.*;

/**
 *
 * @author TimotiusNC
 */
public class CloudClientController implements SavePayloadAdapter {
	public String	m_host = "localhost";
	public int		m_port = 8787;

	private CloudClientController(){
		try {
			m_sock	= new Socket(m_host, m_port);

			m_is	= m_sock.getInputStream();
			m_dis	= new DataInputStream(m_is);
			
			m_os	= m_sock.getOutputStream();
			m_dos	= new DataOutputStream(m_os);

			m_msg	= new Msg();

			initComp();
		} catch (UnknownHostException ex) {
			Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
		} catch (IOException ex) {
			Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
		}
	}

	public static CloudClientController getInstance(){
		if(m_instance == null)
			m_instance = new CloudClientController();

		return m_instance;
	}

	private void initComp(){
		//m_tabModel	= new DefaultTableModel(new Object[]{"File Name", "Last Modified"}, 0);
		m_currFolder	= null;
		m_fileHash		= new ConcurrentHashMap();
		m_isSynching	= false;
		m_revision		= 0; //Default 0
	}

	public void closeSocket(){
		try {
			m_sock.close();
		} catch (IOException ex) {
			Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
		}

		//Destroy this instance
		m_instance = null;

		System.gc();
	}

	public boolean login(String username, String password){
		boolean retval = false;

		LoginPayload loginPayload = new LoginPayload();
		loginPayload.username = username;
		loginPayload.password = password;

		try {
			m_msg.write(loginPayload, m_dos);	//Sending username and password
			m_msg.initRead(m_dis);				//Init response

			byte response = m_msg.getID(); //Get response
			if(response == Payload.OK){ //Login successfull
				initHashMap();
				retval = true;
			} else if(response == Payload.ERROR) {
				retval = false;
			}
		} catch (IOException ex) {
			ex.printStackTrace();
		}

		return retval;
	}

	public void logout(){
		if(m_isSynching){
			stopSync();
		}

		//Close socket
		closeSocket();
	}

	public void initHashMap(){
		if(m_currFolder != null){
			//Check for .cfi file existence first
			File cfiFile = new File(m_currFolder + "/" + Utilities.m_fileHashName);
			if(cfiFile.exists()){
				m_fileHash = readFileHash(cfiFile.getAbsolutePath());
			} else { //Not exist, create it
				flushFileHash(m_fileHash);
			}
		} else {
			System.out.println("m_currFolder is nulll");
		}

		Utilities.getInstance().debugHashMap(m_fileHash);
	}

	public void compareHashMap(){
		System.out.println("\tcompareHashMap");

		//Get list of files inside m_currFolder
		ArrayList<File> listOfFiles = new ArrayList<File>();
		Utilities.getInstance().getListOfFile(listOfFiles, m_currFolder);

		//ADD or MODIFY thigny. Put into temporary HashMap (tempFile dibandingin ke m_fileHash)
		ConcurrentHashMap tempFileHash = new ConcurrentHashMap();
		for(int i=0; i<listOfFiles.size(); ++i)
			tempFileHash.put(listOfFiles.get(i), new Long(listOfFiles.get(i).lastModified()));
		Set keys = tempFileHash.keySet();
		Iterator keysIt = keys.iterator();

		while(keysIt.hasNext()){
			File file = (File) keysIt.next();
			Long newLastMod = new Long(0), oldLastMod = new Long(0); //Initially same

			if(!file.getName().equals(Utilities.m_fileHashName)){
				if(m_fileHash.containsKey(file)){ //File already exist, check last modified
					newLastMod = (Long) tempFileHash.get(file);
					oldLastMod = (Long) m_fileHash.get(file);
				}

				if(!newLastMod.equals(oldLastMod) || !m_fileHash.containsKey(file)) {//Lastmod different OR File not exist, MODIFY / ADD
					System.out.println("\tnew = " + newLastMod + "# old = " + oldLastMod);
					System.out.println("\tADD FILE " + file.getAbsolutePath());
					SavePayload p = new SavePayload(this);
					p.filename = file.getAbsolutePath().substring(m_currFolder.getAbsolutePath().length());

					try {
						//Send the file
						m_msg.write(p, m_dos);
						tempFileHash.put(file, new Long(file.lastModified())); //re-update the hashmap (in-case copy procress took too long)

						//Save the returned revision and update to latest rev if necessary
						int resultRev = getResponseRevision();

						//Check if need to change to latest revision
						if(m_revision + 1 < resultRev)
							gotoLatestRevision(resultRev - 1);

						//Update revision
						m_revision = resultRev;
					} catch (IOException ex) {
						Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
					}
				}
			}
		}

		//DELETE thingy (m_fileHash dibandingin ke tempFileHash)
		keys	= m_fileHash.keySet();
		keysIt	= keys.iterator();

		while(keysIt.hasNext()){
			File file = (File) keysIt.next();
			if(!file.getName().equals(Utilities.m_fileHashName)){
				if(!tempFileHash.containsKey(file)){ //the new hash does not contain the file, so DELETE IT
					System.out.println("\tDELETE FILE " + file.getAbsolutePath());
					DeletePayload dp = new DeletePayload();
					dp.filename = file.getAbsolutePath().substring(m_currFolder.getAbsolutePath().length());

					try {
						//Delete the file
						m_msg.write(dp, m_dos);

						//Get response revision and update
						int resultRev = getResponseRevision();
						m_revision = resultRev;
					} catch (IOException ex) {
						Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
					}
				}
			}
		}

		m_fileHash = tempFileHash;
		flushFileHash(m_fileHash);
		Utilities.getInstance().debugHashMap(m_fileHash);
	}

	/**
	 * 
	 * @return the latest revision number from server
	 */
	public int getLatestRevision(){
		System.out.println("\tgetLatestRevision");
		int retval = -1; //Default -1

		try {
			m_msg.write(Payload.GETREV, m_dos);
			m_msg.initRead(m_dis);

			RevInfoPayload rP = new RevInfoPayload(0); //0->dummy
			m_msg.read(rP, m_dis);

			retval = rP.rev;
		} catch (IOException ex) {
			Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
		}

		System.out.println("\tlatestRev = " + retval);
		return retval;
	}

	/**
	 * 
	 * @param revision number to go to
	 */
	public void gotoLatestRevision(int to){
		System.out.println("\tgotoLatestRevision from " + m_revision + " to " + to);

		GotoRevPayload g = new GotoRevPayload();
		g.from	= m_revision;
		g.to	= to;

		m_revision = to;
		
		try {
			m_msg.write(g, m_dos);

			do{
				System.out.println("masuk do");
				m_msg.initRead(m_dis);
				System.out.println("setelah initread");
				if(m_msg.getID() == Payload.SAVE){
					//Save file
					SavePayload sp = new SavePayload(this);
					m_msg.read(sp, m_dis);

					System.out.println("\tSave = " + sp.filename);

					//Update .cfi file
					m_fileHash.put(this.getFile(), new Long(this.getFile().lastModified()));
				} else if (m_msg.getID() == Payload.DELETE){
					//Delete file
					DeletePayload dp = new DeletePayload();
					m_msg.read(dp, m_dis);

					System.out.println("\tDelete = " + dp.filename);

					//Delete the file
					File toDel = new File(m_currFolder + "/" + dp.filename);
					m_fileHash.remove(toDel);
					toDel.delete();
				}
			}while(m_msg.getID() != Payload.OK && m_msg.getID() != Payload.ERROR);
			System.out.println("\tSELESAI gotoLatestRevision");

			//Flush newly in-memory-hash to .cfi
			flushFileHash(m_fileHash);
		} catch (IOException ex) {
			Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
		}
	}

	/**
	 * Get revision as response from SAVE or DELETE action
	 * @return the revision response
	 */
	public int getResponseRevision(){
		RevInfoPayload rp = new RevInfoPayload(0); //0->dummy
		
		try {
			m_msg.initRead(m_dis);
			m_msg.read(rp, m_dis);
		} catch (IOException ex) {
			Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
		}

		return rp.rev;
	}

	/**
	 * Flush hash map to external file
	 * @param hmap hashmap to be written
	 */
	public void flushFileHash(ConcurrentHashMap hmap){
		//<nama>|<long>
		if(hmap != null && m_currFolder != null){
			StringBuilder sb = new StringBuilder();
			Set keys = hmap.keySet();
			Iterator keysIt = keys.iterator();

			//Write the revision number
			sb.append(m_revision).append("\r\n");
			while(keysIt.hasNext()){
				File file	= (File) keysIt.next();
				String time	= hmap.get(file).toString();

				//Write file absolute path and lastmodified
				if(!file.getName().equals(Utilities.m_fileHashName)){
					sb.append(file.getAbsolutePath()).append("|").append(time).append("\r\n");
				}
			}

			File fileHash = new File(m_currFolder + "/" + Utilities.m_fileHashName);
			FileOutputStream fos = null;
			try {
				fos = new FileOutputStream(fileHash);
				fos.write(sb.toString().getBytes());
			} catch (FileNotFoundException ex) {
				Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
			} catch (IOException ex) {
				Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
			} finally {
				try {
					fos.flush();
					fos.close();
				} catch (IOException ex) {
					Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
				}
			}
		}
	}

	/**
	 * Convert .cfi file into HashMap and also read the revision
	 * @param cfiFilePath .cfi file path
	 * @return the hash map from .cfi file
	 */
	public ConcurrentHashMap readFileHash(String cfiFilePath){
		if(m_currFolder != null){
			ConcurrentHashMap retval	= new ConcurrentHashMap();
			FileInputStream fis			= null;
			DataInputStream dis			= null;
			BufferedReader bf			= null;
			int i = 0;

			try {
				fis	= new FileInputStream(cfiFilePath);
				dis	= new DataInputStream(fis);
				bf	= new BufferedReader(new InputStreamReader(dis));

				String strLine;
				while((strLine = bf.readLine()) != null){
					if(i == 0){ //Get revision number
						m_revision = Integer.parseInt(strLine);
					} else { //Get file name and last modified
						String[] strs = strLine.split("[|]");
						retval.put(new File(strs[0]), new Long(strs[1]));
					}
					++i;
				}
			} catch (FileNotFoundException ex) {
				Logger.getLogger(CloudClientController.class.getName()).log(Level.SEVERE, null, ex);
			} catch (IOException ex){
				ex.printStackTrace();
			}

			return retval;
		}

		return null;
	}

	public void startSync(CloudClientRunnableListener listener){
		m_isSynching = true;
		m_syncRunnable = new CloudClientRunnable(this, listener);
		new Thread(m_syncRunnable).start();
	}

	public void stopSync(){
		m_isSynching = false;
		m_syncRunnable.stop();
	}

	// <editor-fold defaultstate="collapsed" desc="DATA GETTER SETTER">
	public void setHost(String host){
		m_host = host;
	}

	public void setPort(int port){
		m_port = port;
	}

	public File getCurrFolder(){
		return m_currFolder;
	}

	public void setCurrFolder(File currFolder){
		System.out.println(currFolder);
		m_currFolder = currFolder;
	}

	public ConcurrentHashMap getFileHash(){
		return m_fileHash;
	}

	public DefaultTableModel getTabModel(){
		return m_tabModel;
	}

	public boolean isSynching(){
		return m_isSynching;
	}

	public void setRev(int rev){
		m_revision = rev;
	}

	public int getRev(){
		return m_revision;
	}
	// </editor-fold>

	// <editor-fold defaultstate="collapsed" desc="GETTER SETTER">
	public Socket getSock() {
		return m_sock;
	}

	public InputStream getIs() {
		return m_is;
	}

	public DataInputStream getDis() {
		return m_dis;
	}

	public OutputStream getOs(){
		return m_os;
	}

	public DataOutputStream getDos(){
		return m_dos;
	}

	public Msg getMsg() {
		return m_msg;
	}
	// </editor-fold>

	//ATTRIBUTES
	private Socket				m_sock;
	private InputStream			m_is;
	private DataInputStream		m_dis;
	private OutputStream		m_os;
	private DataOutputStream	m_dos;
	private Msg					m_msg;

	//FOLDER
	private File	m_currFolder;

	//DATA
	private ConcurrentHashMap	m_fileHash;
	private DefaultTableModel	m_tabModel;
	private CloudClientRunnable m_syncRunnable;
	private boolean				m_isSynching;
	private int					m_revision;

	//INSTANCE
	private static CloudClientController m_instance;

	//SavePayloadADAPTER thingy
	private File	m_curFile;
	private String	m_realFileName;

	/**
	 * Read file to be SENT TO SERVER
	 * @param filename filename to get from server
	 */
	public void initWrite(String filename) {
        //Get the file
        m_curFile = new File(m_currFolder + "/" + filename);
		System.out.println("\tinitWrite = " + m_curFile.getAbsolutePath());

        //If file is not existed, error
        if(!m_curFile.exists()) {
            System.out.println("\tFile not exist.");
        }
	}

	/**
	 * Read file FROM SERVER, SAVE it to client
	 * New file will be added to client
	 * @param filename
	 */
	public void initRead(String filename) {
		//Save the real file name
		m_realFileName = m_currFolder + "/" + filename;
		System.out.println("\tinitRead = " + m_realFileName);

		//Create directory first if necessary
		int lastIndex = m_realFileName.lastIndexOf("/");
		if (lastIndex!=-1) {
			String dir = m_realFileName.substring(0, lastIndex);
			new File(dir).mkdirs();
		}

		//Generate file with random, temporary filename
		m_curFile = new File(m_realFileName);
	}

	/**
	 * 
	 * @return the file this adapter has written
	 */
	public File getFile() {
		return m_curFile;
	}
}
